
#include <stdlib.h>
#include <string.h>
#include "spi.h"
#include "stm32f10x_spi.h"
#include "device.h"

extern int device_find_by_class_port(dev_class_e class, int port);

struct spi_hw_config
{
    spi_port_e port;
    SPI_TypeDef *SPIx;
    GPIO_TypeDef *GPIOx;
	uint16_t SPIx_SCK_Pin;
    uint16_t SPIx_MISO_Pin;
	uint16_t SPIx_MOSI_Pin;
};

const struct spi_hw_config spi_hw_config_list[SPI_PORT_MAX] = {
    {SPI_PORT0, SPI1, GPIOA, GPIO_Pin_5, GPIO_Pin_6, GPIO_Pin_7},
    {SPI_PORT1, SPI2, GPIOB, GPIO_Pin_13, GPIO_Pin_15, GPIO_Pin_14},
    {SPI_PORT2, SPI3, GPIOB, GPIO_Pin_3, GPIO_Pin_4, GPIO_Pin_5}
};

static int spi_add_cs_pin(spi_port_e port, gpio_pin_e pin) {
    spi_t *spi = spi_find_by_port(port);
    for(int i=0; i<sizeof(spi->cs_pin); i++) {
        if(spi->cs_pin[i] == -1) {
            spi->cs_pin[i] = pin;

            gpio_cfg_t cs_gpio_cfg = {GPIO_MODE_OUT_PP};
            gpio_t *cs_gpio= gpio_find_by_pin(pin);
            cs_gpio->open(&cs_gpio_cfg);
            cs_gpio->write(1);
        
            return 0;
        }
    }

    return -1;
}

static int spi_open(spi_port_e port, spi_cfg_t *config) {
    if(!config) return -1;

    spi_t *spi = spi_find_by_port(port);

    uint16_t dir;
    switch(config->dir) {
        case SPI_DIR_2LINES_FULL_DUPLEX: dir = SPI_Direction_2Lines_FullDuplex; break;
        case SPI_DIR_2LINES_RX_ONLY: dir = SPI_Direction_2Lines_RxOnly; break;
        case SPI_DIR_1LINE_RX: dir = SPI_Direction_1Line_Rx; break;
        case SPI_DIR_1LINE_TX: dir = SPI_Direction_1Line_Tx; break;
        default: return -1;
    }

    uint16_t mode;
    switch(config->mode) {
        case SPI_MODE_MASTER: mode = SPI_Mode_Master; break;
        case SPI_MODE_SLAVE: mode = SPI_Mode_Slave; break;
        default: return -1;
    }

    uint16_t cpol;
    switch(config->cpol) {
        case SPI_CPOL_LOW: cpol = SPI_CPOL_Low; break;
        case SPI_CPOL_HIGH: cpol = SPI_CPOL_High; break;
        default: return -1;
    }

    uint16_t cpha;
    switch(config->cpha) {
        case SPI_CPHA_1EDGE: cpha = SPI_CPHA_1Edge; break;
        case SPI_CPHA_2EDGE: cpha = SPI_CPHA_2Edge; break;
        default: return -1;
    }

    uint16_t pre_scaler;
    if(port == SPI_PORT0) {
        if(config->freq == SPI_FREQ_140K625) return -1;
        pre_scaler = ((config->freq + 2) / 2 - 1) << 4 | (config->freq % 2 ? 0x8 : 0x0);
    } else {
        if(config->freq == SPI_FREQ_36M) return -1;
        pre_scaler = ((config->freq + 1) / 2 - 1) << 4 | (config->freq % 2 ? 0x0 : 0x8);
    }

    uint16_t first_bit;
    switch(config->first_bit) {
        case SPI_1ST_BIT_MSB: first_bit = SPI_FirstBit_MSB; break;
        case SPI_1ST_BIT_LSB: first_bit = SPI_FirstBit_LSB; break;
        default: return -1;
    }

    switch(port) {
        case SPI_PORT0: RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); break;
        case SPI_PORT1: RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); break;
        case SPI_PORT2: RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE); break;
        default: return -1;
    }

    RCC_APB2PeriphClockCmd(1 << (((uint32_t)(spi_hw_config_list[spi->port].GPIOx) - (uint32_t)APB2PERIPH_BASE) >> 10), ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = spi_hw_config_list[spi->port].SPIx_SCK_Pin | spi_hw_config_list[spi->port].SPIx_MISO_Pin | spi_hw_config_list[spi->port].SPIx_MOSI_Pin;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(spi_hw_config_list[spi->port].GPIOx, &GPIO_InitStructure); 

    SPI_InitTypeDef  SPI_InitStructure;
	SPI_InitStructure.SPI_Direction = dir;
	SPI_InitStructure.SPI_Mode = mode;
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
	SPI_InitStructure.SPI_CPOL = cpol;
	SPI_InitStructure.SPI_CPHA = cpha;
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
	SPI_InitStructure.SPI_BaudRatePrescaler = pre_scaler;
	SPI_InitStructure.SPI_FirstBit = first_bit;
	SPI_InitStructure.SPI_CRCPolynomial = 7;
	SPI_Init(spi_hw_config_list[port].SPIx, &SPI_InitStructure);

    SPI_Cmd(spi_hw_config_list[port].SPIx, ENABLE);

    memcpy(&spi->cfg, config, sizeof(spi_cfg_t));

    return 0;
}

static void spi_take(spi_port_e port, gpio_pin_e pin) {
    spi_t *spi = spi_find_by_port(port);
    gpio_t *cs_gpio;
    for(int i=0; i<sizeof(spi->cs_pin); i++) {
        if(spi->cs_pin[i] == pin) {
            cs_gpio = gpio_find_by_pin(pin);
            cs_gpio->write(0);
            break;
        }
    }
}

static void spi_release(spi_port_e port, gpio_pin_e pin) {
    spi_t *spi = spi_find_by_port(port);
    gpio_t *cs_gpio;
    for(int i=0; i<sizeof(spi->cs_pin); i++) {
        if(spi->cs_pin[i] == pin) {
            cs_gpio = gpio_find_by_pin(pin);
            cs_gpio->write(1);
            break;
        }
    }
}

static void spi_write_read(spi_port_e port, uint8_t *wdata, uint8_t *rdata, int len) {
    SPI_TypeDef *SPIx = spi_hw_config_list[port].SPIx;
    for(int i=0; i<len; i++) {
        while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_TXE) == RESET);
        SPI_I2S_SendData(SPIx, (uint16_t)wdata[i]);
        while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) == RESET);
        if(rdata) {
            rdata[i] = (uint8_t)SPI_I2S_ReceiveData(SPIx);
        }
    }
}

static void spi_close(spi_port_e port) {
    SPI_Cmd(spi_hw_config_list[port].SPIx, DISABLE);
}

static void spi_deinit(spi_port_e port) {
    spi_t *spi = spi_find_by_port(port);
    spi_close(port);
    free(spi);
}

#define SPI_ADD_CS_PIN(e) int e##_ADD_CS_PIN (gpio_pin_e pin) { \
    return spi_add_cs_pin(e, pin); \
}

#define SPI_OPEN_FNC(e) int e##_OPEN (spi_cfg_t *cfg) { \
    return spi_open(e, cfg); \
}

#define SPI_TAKE_FNC(e) void e##_TAKE (gpio_pin_e pin) { \
    spi_take(e, pin); \
}

#define SPI_RELEASE_FNC(e) void e##_RELEASE (gpio_pin_e pin) { \
    spi_release(e, pin); \
}

#define SPI_WRITE_READ_FNC(e) void e##_WRITE_READ (uint8_t *wdata, uint8_t *rdata, int len) { \
    spi_write_read(e, wdata, rdata, len); \
}

#define SPI_CLOSE_FNC(e) void e##_CLOSE (void) { \
    spi_close(e); \
}

#define SPI_DEINIT_FNC(e) void e##_DEINIT (void) { \
    spi_deinit(e); \
}

SPI_PORT_LIST(SPI_ADD_CS_PIN)
SPI_PORT_LIST(SPI_OPEN_FNC)
SPI_PORT_LIST(SPI_TAKE_FNC)
SPI_PORT_LIST(SPI_RELEASE_FNC)
SPI_PORT_LIST(SPI_WRITE_READ_FNC)
SPI_PORT_LIST(SPI_CLOSE_FNC)
SPI_PORT_LIST(SPI_DEINIT_FNC)

#define SPI_METHOD_PROC(e) case e: { \
    spi->add_cs_pin = e##_ADD_CS_PIN; \
    spi->open = e##_OPEN; \
    spi->take = e##_TAKE; \
    spi->release = e##_RELEASE; \
    spi->write_read = e##_WRITE_READ; \
    spi->close = e##_CLOSE; \
    spi->deinit = e##_DEINIT; \
    break; \
}

spi_t *spi_init(spi_port_e port) {
    if(port >= SPI_PORT_MAX) return NULL;

	spi_t *spi = malloc(sizeof(spi_t));
	if(!spi) return NULL;

	memset(spi, 0, sizeof(spi_t));
    memset(spi->cs_pin, -1, sizeof(spi->cs_pin));

    spi->port = port;

    switch(port) {
        SPI_PORT_LIST(SPI_METHOD_PROC)
        default: break;
    }

    return spi;
}

spi_t *spi_find_by_port(spi_port_e port) {
    return (spi_t *)device_find_by_class_port(DEV_CLASS_SPI, (int)port);
}
